WASM-моддинг для ваших игр на Godot .NET
Содержание
 
    
При разработке Arcomage с использованием Godot .NET я столкнулся с проблемой: как добавить поддержку моддинга на основе скриптов в игру? Проблема заключалась в том, что я не хотел разрешать прямую инъекцию DLL — это крайне небезопасно и подвержено злоупотреблениям, позволяющим внедрять вредоносный код, который может скомпрометировать системы пользователей или украсть конфиденциальные данные, такие как пароли.
Вместо этого я выбрал подход с использованием песочницы, при котором моды выполняются безопасно и не представляют угрозы для конечных пользователей. Моя цель состояла в создании единого API для всех модов, чтобы сделать их не только безопаснее, но и проще в написании. Именно тогда я наткнулся на видео от безымянного разработчика, в котором чётко и понятно объясняется, как реализовать поддержку модов на WASM. Огромное спасибо автору за его идеи!
Я уже знал о WebAssembly, но никогда не рассматривал его как песочницу для запуска скриптовых модов в игре. Было удивительно узнать, что Microsoft Flight Simulator 2020 использует WebAssembly для скриптов модов — и это действительно круто!
Итак, я решил попробовать нечто подобное для Arcomage. В этой статье я поделюсь своими находками и идеями по реализации WASM-моддинга в Godot .NET.
Что такое WebAssembly и как им пользоваться? #
WASM — это формат байткода, который выполняется в виртуальной машине. В отличие от CIL .NET, WASM предназначен для исполнения в браузерах или в любых приложениях, которые его поддерживают. В нашем случае мы используем версию Godot с поддержкой .NET. Для .NET существует несколько встроенных WASM-рантаймов. В моем проекте я использовал Wasmtime от Bytecode Alliance.
Пример: Создание WASM-мода #
Настройка #
Сначала добавьте пакет Wasmtime через NuGet в ваш проект Godot .NET:
dotnet add package wasmtime
Либо добавьте следующую ссылку на пакет в ваш файл .csproj:
<PackageReference Include="Wasmtime" Version="22.0.0" />
Не забудьте выполнить dotnet restore после внесения изменений в файл проекта.
Чтобы компилировать файлы WASM из AssemblyScript, установите Node.js (в комплекте с npm). Вы можете скачать его с официального сайта Node.js. Проверьте установку с помощью команд:
node -v
npm -v
В этом примере мы будем компилировать WASM с помощью AssemblyScript. Если вы предпочитаете другой язык (например, Rust с wasm-pack), ознакомьтесь с его документацией. Также настоятельно рекомендую изучить документацию компилятора AssemblyScript.
После установки Node.js выполните:
npm install -g assemblyscript
Эта команда устанавливает компилятор AssemblyScript глобально (либо локально, если предпочитаете).
После установки необходимых инструментов, пора писать код!
WASM #
Выберите рабочую директорию и создайте два файла: env.ts и mod.ts.
В файле env.ts определите экспортируемые функции, которые будут доступны вашему WASM-моду:
@external("env", "host_log")
declare function host_log(ptr: i32, len: i32): void;
export function log(message: string): void {
  const encoded = String.UTF8.encode(message);
  host_log(changetype<i32>(encoded), encoded.byteLength);
}
В этом примере функция log отправляет сообщение хосту через host_log, которая принимает указатель на строку, закодированную в UTF-8, и её длину.
В файле mod.ts напишите код, который будет выполняться в WASM-моде:
import { log } from "./env";
export function init(): void {
  log("Hello, World!");
}
Здесь экспортированная функция init будет вызвана при инициализации мода, что вызовет отправку лог-сообщения. На стороне Godot вы обработаете это (например, выведя сообщение в консоль с помощью GD.Print).
Эта простая настройка иллюстрирует концепцию. Как разработчик, поделитесь файлом env.ts с мододелами, чтобы они могли использовать определённые функции. В этой схеме mod.ts служит точкой входа для мода, где вы указываете функцию входа (в данном случае, init), которая автоматически вызывается игрой при загрузке WASM-файла.
Чтобы скомпилировать ваш WASM-мод, выполните:
asc .\mod.ts -o mod.wasm
Если AssemblyScript не установлен глобально, убедитесь, что вы указали корректный путь. Выполнение этой команды создаёт файл mod.wasm — точку входа для вашего мода.
Интеграция с Godot .NET #
Поместите скомпилированный WASM-файл в директорию user вашего проекта Godot (например, user://mod.wasm).
Рекомендую использовать пользовательскую директорию, включив её с помощью флага application/config/use_custom_user_dir в файле project.godot. Также задайте имя папки через application/config/custom_user_dir_name.
Создайте новый C# класс, наследующий от Node — в моем примере он называется WasmLoader:
using Godot;
using System.Text;
using Wasmtime;
using Engine = Wasmtime.Engine;
public partial class WasmLoader : Node
{
   public override void _Ready()
   {
      byte[] wasmBytes;
      using (var file = FileAccess.Open("user://mod.wasm", FileAccess.ModeFlags.Read))
         wasmBytes = file.GetBuffer((int)file.GetLength());
      using var engine = new Engine();
      using var module = Module.FromBytes(engine, "mod", wasmBytes);
      using var store = new Store(engine);
      using var linker = new Linker(engine);
      linker.Define("env", "host_log", Function.FromCallback(store, (Caller caller, int ptr, int len) =>
      {
         var memory = caller.GetMemory("memory");
         if (memory is null)
            return;
         var span = memory.GetSpan<byte>(0);
         var message = Encoding.UTF8.GetString(span.Slice(ptr, len).ToArray());
         GD.Print(message);
      }));
      linker.Define("env", "abort", Function.FromCallback(store, (int msg, int file, int line, int column) =>
      {
         GD.Print($"Abort called at {file}:{line}:{column}");
      }));
      var instance = linker.Instantiate(store, module);
      instance.GetAction("init")?.Invoke();
   }
}
Разберем, что происходит в этом коде:
- WASM-файл загружается из user://mod.wasmи его байты передаются вModule.FromBytes.
- Создаются Engine,Module,StoreиLinkerдля выполнения WASM-мода.
- Определяются функции, экспортируемые модом. Здесь host_logпринимает указатель и длину, декодирует строку из памяти и выводит её.
- Также определяется функция abort; она вызывается при ошибках внутри WASM-мода. Хотя она может и не понадобиться, компилятор включает её по умолчанию. Если не определить её, вы получите исключениеWasmtime.WasmtimeExceptionиз-за неопределённого импорта.
- Наконец, WASM-мод инициализируется, и вызывается его функция init, которая выводит “Hello, World!” в консоль.
После создания класса WasmLoader добавьте его в сцену и запустите игру. Если всё настроено правильно, вы увидите “Hello, World!” в консоли. Либо вы можете добавить загрузчик в Autoload вашего проекта, чтобы он запускался при старте.
Следующие шаги #
Вот несколько идей для расширения вашей системы моддинга:
- 
Передача обратных вызовов процесса: 
 Передавайте обратные вызовы_Processи_PhysicsProcessв WASM-мод (не забудьте включить параметрdelta), чтобы он мог взаимодействовать с игрой каждый кадр — например, обновлять позиции объектов:public override void _Process(double delta) { foreach (var mod in _mods.Values) mod.Instance.GetFunction("process")?.Invoke(delta); }Здесь _mods— этоDictionary, хранящий загруженные моды, где ключом является имя мода, а значением — запись (содержащая экземпляр мода и его путь). Аналогично можно передавать события (например, событие выхода), когда игра закрывается или мод выгружается.
- 
Создание API для модов: 
 Разработайте API для модов, которое будет предоставлять функции (например,log,spawn,destroy,move) для мододелов. Стремитесь к простоте и ясности, а также подробно задокументируйте API, чтобы мододелы знали, что доступно и как это использовать.
- 
Автоматическая загрузка модов: 
 Реализуйте функциональность для загрузки WASM-модов из определенной папки, позволяя мододелам просто помещать свои файлы модов в папку для автоматической загрузки. Также рассмотрите возможность добавления поддержки выгрузки модов, чтобы избежать проблем с производительностью при слишком большом количестве активных модов. Полноценный интерфейс управления модами (например, интегрированный в меню игры) станет отличным улучшением.
Надеюсь, эта небольшая статья поможет вам реализовать WASM-моддинг в ваших играх на Godot .NET. Если у вас возникнут вопросы или потребуется дополнительная помощь, не стесняйтесь оставить комментарий — я постараюсь помочь!